Chrome拡張機能とローカルアプリでプロセス間通信
Introduction
Native messagingとは、ユーザーのPCにインストールされたアプリと
Webブラウザの拡張機能間でメッセージ交換を可能にする機能です。
この機能を使えば、ネイティブアプリケーションが
Web経由でアクセスできなくても拡張機能にサービスを提供できます。
例えば、ローカルアプリがパスワードの暗号化と保管を行い、
拡張機能へパスワードを送信してフォームに自動設定、みたいなことも可能です。
また、拡張機能からは通常アクセスできないリソースに対しても実質的にアクセス可能になります。
本稿ではGoogle Chromeの拡張機能(extention)と
ローカルに用意したNodeプログラムでプロセス間通信をためしてみます。
※現状ではChrome以外でもEdge/Firefox/Safariなど主要なプラウザで使用可能
Environment
- OS : MacOS 10.15.7
- Node : v18.3.0
- Chrome : 103.0.5060.114
Native Massage Architecture
ローカルアプリとextentionで通信するためのNative Message機能ですが、
仕組みは特に難しくありません。
↓の図のように、ブラウザの拡張機能とローカルアプリをmanifest.jsonで関連付け、
chromeのapiと標準入出力でメッセージのやりとりをします。
このmanifest.json(拡張機能のmanifest.jsonとは別)が重要で、
ここでアクセスを許可する拡張機能のIDを指定や、
拡張機能からアクセスするための名前を指定します。
ローカルのアプリはそのまま実行できる形式であればOKで、
exe(Windows)形式やpythonのファイルなどが指定可能です。
今回はshebangをつけたjsファイルを作成します。
Try
Chrome Extensionをつくる
まずはChrome拡張を作成します。
ここで作成するExtensionは、
service workerでネイティブアプリに対して
メッセージを送受信するだけのものです。
Extension用ディレクトリを作成し、
そこにChrome Extension用のmanifest.jsonを作成します。
{ "manifest_version": 3, "name": "Native Messaging Example", "description": "Native Messaging Example chrome extension using manifest v3", "version": "0.0.1", "action": { "default_title": "Native Messaging Example", "default_popup": "popup.html" }, "permissions": ["nativeMessaging"], "host_permissions": [ "*://*/*" ], "background": { "service_worker": "service-worker.js" } }
permissionsにnativeMessagingを指定しています。
これがないとNative Messageが使えません。
通信処理は後述するservice-worker.jsに記述します。
今回はとくに関係ないけど、
ボタン押したときに起動するpopup.htmlを作成。
<!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> <title>Native Messaging Example</title> </head> <body> This is Native Messaging Example. </body> </html>
Netive Messageを行うservice-worker.jsを
作成します。
//ローカルアプリの起動 var port = chrome.runtime.connectNative('my_native_message_example') //ローカルアプリからメッセージ受信 port.onMessage.addListener((req) => { if (chrome.runtime.lastError) { console.log(chrome.runtime.lastError.message) } handleMessage(req) }) //アプリから切断されたときの処理 port.onDisconnect.addListener(() => { if (chrome.runtime.lastError) { console.log(chrome.runtime.lastError.message) } console.log('Disconnected') }) function handleMessage (req) { console.log("req.message : " + req.message); if (req.message === 'pong') { console.log(req) } } //ローカルアプリへメッセージ送信 port.postMessage({message: 'ping', body: 'hello from browser extension'})
chrome.runtime.connectNativeでアプリと接続し、
メッセージの送受信処理を記述します。
ここで指定している「my_native_message_example」という名前が、
後述するmanifestファイルで指定するプロセス通信名となります。
ここまでできたら、chrome://extensions/にアクセスし、
「パッケージ化されていない拡張機能を読み込む」ボタンを押して
Extentionを登録します。
すると、↓のようにIDが表示されるので、
控えておきましょう。
なお、現時点でService Workerのリンクをクリックしても
ホスト側のアプリがないのでエラーがでます。
manifestファイルの用意
Native Messageで通信するため、ローカルのアプリ本体とは別に
JSON形式のmanifestファイルが必要になります。
このマニフェストファイルの名前とnameの値は、
さきほどconnectNativeで指定した名前と同じにします。
my_native_message_example.jsonという名前をつけ、
下記内容を記述します。
{ "name": "my_native_message_example", "description": "Native Message example", "path":"<ローカルアプリへのフルパス>", "type": "stdio", "supports_native_initiated_connections": true, "allowed_origins": ["chrome-extension://<Chrome拡張のID>/"] }
pathはこのあと作成するローカルアプリのフルパスです。
また、allowed_originsにはさきほど控えたIDを指定します。
※最後の/がないとエラーになるっぽいので注意
manifestファイルを作成したら、決められた位置に置きます。
ここにあるように、
/<ユーザーディレクトリ>/Library/ApplicationSupport/Google/Chrome/NativeMessagingHosts
にmanifestファイル(↑の例だとmy_native_message_example.json)を
配置します。
このファイルを置く位置は、対象ブラウザやOSによって違うので
リンク先を確認してください。
ローカルアプリの作成
manifestで指定したpathの場所に実行可能なアプリを作成します。
今回はNodeで動くjsファイルを作成しました。
#!/usr/bin/node //標準入力処理 process.stdin.on('readable', () => { var input = [] var chunk while (chunk = process.stdin.read()) { input.push(chunk) } input = Buffer.concat(input) var msgLen = input.readUInt32LE(0) var dataLen = msgLen + 4 if (input.length >= dataLen) { var content = input.slice(4, dataLen) var json = JSON.parse(content.toString()) handleMessage(json) } }) function handleMessage (req) { if (req.message === 'ping') { sendMessage({message: 'pong', body: 'hello from nodejs app',ping_body:req.body}) } } //標準出力処理 function sendMessage(msg) { var buffer = Buffer.from(JSON.stringify(msg)) var header = Buffer.alloc(4) header.writeUInt32LE(buffer.length, 0) var data = Buffer.concat([header, buffer]) process.stdout.write(data) } //エラー処理 process.on('uncaughtException', (err) => { sendMessage({error: err.toString()}) })
1行目のshebangには、自身の環境のnodeのパスを指定してください。
アプリ側では、標準入力を用いてメッセージの受信、
標準出力でメッセージを送信しています。
メッセージはシリアライズ、UTF-8エンコード、
メッセージ長の付加をして使用しています。
動作確認
nodeアプリができて、manifestファイルのpathで指定した位置に配置したら
Chrome Extensionを更新(再度インストール)します。
Service Workerリンクを開いてみると、
↓のようにメッセージのやり取りができているのがわかります。
body: "hello from nodejs app" message: "pong" ping_body: "hello from browser extension"
また、このタイミングでpsコマンドでnodeのプロセスをみてみると、
起動されたプロセスがわかります。
% ps -ax | grep node 27211 /usr/bin/node /path/your/script.js chrome-extension://<chrome extension id>/
上記プロセスをkillすると、ウィンドウにDisconnectedメッセージが表示されます。
Summary
今回はChrome Extensionとローカルアプリの通信について紹介しました。
Extensionだけでは実現できない機能も、
この仕組みをつかえば実現できる(かもしれない)。